import { Download, ExternalLink } from 'lucide-react'; import type { Metadata } from 'next'; import Link from 'next/link'; import { notFound } from 'next/navigation'; import { t } from '@/i18n'; import { api, isNotBuilt, isNotFound, safe } from '@/lib/api'; import { apiAnalytics } from '@/lib/api-analytics'; import { apiCompare } from '@/lib/api-compare'; import { grouped } from '@/lib/format'; import { parseRankingState, type RankingState } from '@/lib/ranking-state'; import { jsonLd, jsonLdString, seoTitle } from '@/lib/seo'; import { routes } from '@/lib/site'; import { topicById } from '@/lib/topics'; import type { RankingResponse } from '@/lib/types'; import { toCountryLite, type CountryLite } from '@/lib/types-compare'; import { RankRace } from '@/components/charts/rank-race'; import { NotBuiltState } from '@/components/data/empty-state'; import { Section } from '@/components/data/section'; import { baseFeatures } from '@/components/indicators/map-geometry'; import { RankHistoryPanel } from '@/components/rankings/rank-history-panel'; import { RankingView } from '@/components/rankings/ranking-view'; export const revalidate = 900; type Params = { indicator: string }; type SP = Record; /** Indicators with a rank race on their page (long histories, ≥ 30 ranked years). */ const RACE_SLUGS = new Set(['gdp', 'gdp-ppp', 'population', 'gdp-per-capita', 'gdp-per-capita-ppp', 'life-expectancy', 'co2-emissions', 'co2-per-capita', 'internet-users', 'exports-goods-services', 'urban-population-share', 'fertility-rate', 'median-age']); async function load(slug: string, state: RankingState): Promise<(RankingResponse & { label?: string }) | 'not-built' | null> { if (!/^[a-z0-9][a-z0-9-]*$/.test(slug)) return null; try { return await apiCompare.ranking(slug, { year: state.year, group: state.group, sort: state.sort, limit: 300, sparkline: true }); } catch (e) { if (isNotFound(e)) return null; if (isNotBuilt(e)) return 'not-built'; throw e; } } export async function generateMetadata({ params, searchParams }: { params: Promise; searchParams: Promise }): Promise { const [{ indicator }, sp] = await Promise.all([params, searchParams]); const state = parseRankingState(sp); const data = await load(indicator, state); if (!data || data === 'not-built') return { title: t('ranking.notFound'), robots: { index: false } }; const name = data.indicator.short_name ?? data.indicator.name ?? indicator; const year = data.year_used ?? ''; const world = state.group === 'world'; const title = world ? seoTitle.ranking(name, year) : t('ranking.pageTitleGroup', { name, group: data.group.name ?? state.group, year }); const top = data.rows .slice(0, 3) .map((r) => r.country.name) .filter(Boolean) .join(', '); const description = t('ranking.pageDescription', { name: data.indicator.name ?? name, n: grouped(data.n), year, top }); const canonical = routes.ranking(data.indicator.slug, { group: state.group }); const volatile = state.year != null || state.highlight || state.q || state.income || state.minpop || state.mincov || state.view !== 'table'; return { title: { absolute: `${title} | ${t('site.name')}` }, description, alternates: { canonical }, robots: volatile ? { index: false, follow: true } : undefined, openGraph: { title: `${title} | ${t('site.name')}`, description, url: canonical, type: 'article' }, twitter: { card: 'summary_large_image', title, description }, }; } export default async function RankingPage({ params, searchParams }: { params: Promise; searchParams: Promise }) { const [{ indicator }, sp] = await Promise.all([params, searchParams]); const state = parseRankingState(sp); const data = await load(indicator, state); if (data === null) notFound(); if (data === 'not-built') return ; const ind = data.indicator; const name = ind.short_name ?? ind.name ?? indicator; const year = data.year_used; const highlightRow = state.highlight ? data.rows.find((r) => (r.country.slug ?? '') === state.highlight) : null; const historyIds = state.history.length ? state.history : Array.from(new Set([...data.rows.slice(0, 3).map((r) => r.country.id), ...(highlightRow ? [highlightRow.country.id] : [])])).slice(0, 5); const wantRace = RACE_SLUGS.has(ind.slug) || !!ind.featured; const [regions, countriesRes, history, race] = await Promise.all([ safe(apiCompare.regions()), safe(api.countries()), historyIds.length ? safe(apiCompare.rankHistory(ind.slug, historyIds)) : Promise.resolve(null), wantRace ? safe(apiAnalytics.race(ind.slug, { top: 10, group: state.group !== 'world' ? state.group : null })) : Promise.resolve(null), ]); const all = (countriesRes?.items ?? []).filter((c) => c.kind !== 'aggregate'); const countries: CountryLite[] = all.map(toCountryLite).sort((a, b) => a.name.localeCompare(b.name)); const { features, sphere } = all.length ? baseFeatures(all) : { features: [], sphere: '' }; const topic = topicById(ind.topic ?? ''); const groupName = data.group.name ?? state.group; const spec = { format: ind.format, unit: ind.unit, unit_short: ind.unit_short, precision: ind.precision, name, higher_is_better: ind.higher_is_better }; const showRace = race && race.frames.length >= 30; const ld = [jsonLd.breadcrumbs([{ name: t('rankings.title'), path: routes.rankings() }, { name, path: routes.ranking(ind.slug) }]), jsonLd.dataset({ slug: ind.slug, name: ind.name ?? name, unit: ind.unit, nCountries: data.n, modified: data.meta.built_at })]; return ( <>